Skip to content

Comments

fix: prevent findNextStop from wrapping to first stop at end of trip#458

Open
aditya-systems-hub wants to merge 4 commits intoOneBusAway:mainfrom
aditya-systems-hub:fix/find-next-stop-wraparound
Open

fix: prevent findNextStop from wrapping to first stop at end of trip#458
aditya-systems-hub wants to merge 4 commits intoOneBusAway:mainfrom
aditya-systems-hub:fix/find-next-stop-wraparound

Conversation

@aditya-systems-hub
Copy link


Purpose of this PR

This PR fixes an issue where findNextStop() incorrectly wraps around to the first stop of a trip when a vehicle is already at its final stop.

Previously, the function used modulo arithmetic:

(i + 1) % len(stopTimes)

When the vehicle was at the last stop, this wrapped the index to 0, returning the first stop of the route as the “next stop.” This incorrect stop ID propagated through BuildTripStatus() and into downstream calculations such as numberOfStopsAway and distanceFromStop, causing incorrect arrival predictions across multiple API endpoints.

This change removes the modulo wrap-around and replaces it with a proper bounds check, ensuring that when a vehicle reaches its final stop, no next stop is returned.


Proposed changes:

  • Replaced modulo index wrap-around with explicit bounds checking in findNextStop().
  • Removed the redundant len(stopTimes) > 0 guard (dead check inside loop).
  • Ensured the function returns "" when the vehicle is at the last stop.
  • Limited changes to a single function in one file for minimal impact.

Change Category

☑ Bugfix (non-breaking change which fixes an issue)
☐ Feature (non-breaking change which adds functionality)
☐ Breaking change (fix or feature that could affect existing functionality)
☐ Documentation update


Rationale

A vehicle at the final stop of its trip should not have a next stop. The previous modulo-based logic incorrectly treated the route as circular, returning the first stop as the next stop.

This resulted in:

  • Incorrect NextStop values
  • Invalid numberOfStopsAway calculations
  • Nonsensical distanceFromStop values
  • Misleading arrival predictions in all affected endpoints

The bug was silent — no crash, no error log — but consistently triggered whenever any vehicle reached its final stop. The fix ensures correct trip completion behavior and prevents corrupted arrival data.


Checklist

☑ I have conducted a self-review of my own code.
☑ I have verified the change compiles cleanly.
☑ Existing unit tests pass locally with my changes.
☐ Documentation update required (not applicable).
☐ New tests added (not required; logic correction only).


Additional Notes

Verification performed:

  • go build ./... passes successfully.
  • go vet ./internal/restapi/... passes with no issues.
  • App package tests pass (TestBlankKeyIsInvalid, TestRequestHasInvalidAPIKey).

The restapi test failure related to missing testdata/raba.zip is a pre-existing environment setup issue and is unrelated to this change.

Scope of change:

  • 1 file modified
  • 1 function updated
  • 7 lines changed

The fix is minimal, safe, and strictly addresses the wrap-around bug without affecting other logic.


Test case

Screenshot 2026-02-23 063210

@CLAassistant
Copy link

CLAassistant commented Feb 23, 2026

CLA assistant check
All committers have signed the CLA.

Copy link
Member

@aaronbrethorst aaronbrethorst left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey Aditya, sharp catch on the modulo wrap-around bug — when a vehicle reaches its last stop, silently returning the first stop as the "next stop" is exactly the kind of subtle data corruption that's easy to miss and hard to debug downstream. The fix is clean and correct. A couple of items before merging:

Important

1. Add a unit test for findNextStop (internal/restapi/trips_helper.go:291)

This function currently has zero test coverage (no tests reference findNextStop anywhere in the codebase). Since this is a bugfix, a regression test would ensure this behavior doesn't silently revert. Three cases would cover it well:

func TestFindNextStop(t *testing.T) {
    api := createTestApi(t)

    stopTimes := []*gtfsdb.StopTime{
        {StopID: "A", StopSequence: 1},
        {StopID: "B", StopSequence: 2},
        {StopID: "C", StopSequence: 3},
    }

    t.Run("returns next stop when not at last stop", func(t *testing.T) {
        seq := uint32(1)
        vehicle := &gtfs.Vehicle{CurrentStopSequence: &seq}
        stopID, _ := findNextStop(api, stopTimes, vehicle)
        assert.Equal(t, "B", stopID)
    })

    t.Run("returns empty when at last stop", func(t *testing.T) {
        seq := uint32(3)
        vehicle := &gtfs.Vehicle{CurrentStopSequence: &seq}
        stopID, _ := findNextStop(api, stopTimes, vehicle)
        assert.Equal(t, "", stopID)
    })

    t.Run("returns empty when vehicle is nil", func(t *testing.T) {
        stopID, _ := findNextStop(api, stopTimes, nil)
        assert.Equal(t, "", stopID)
    })
}

Fit and Finish

2. api parameter is unused (trips_helper.go:292)

The api *RestAPI parameter isn't referenced anywhere in findNextStop. This is a pre-existing issue (not introduced by your PR), so fixing it is optional — but since you're already touching this function, removing it would clean things up. If you do, update the call site at line 102:

// Line 102, before:
nextStopID, nextOffset = findNextStop(api, stopTimesPtrs, vehicle)

// After:
nextStopID, nextOffset = findNextStop(stopTimesPtrs, vehicle)

Verification

  • make test: all packages pass
  • make lint: 0 issues
  • gofmt -d: no formatting differences
  • Single commit: d7a3305 fix: prevent findNextStop from wrapping to first stop at end of trip

Strengths

  • Correct diagnosis: The PR description clearly explains the bug, the root cause (modulo wrap-around), and the downstream effects (incorrect numberOfStopsAway, distanceFromStop, arrival predictions). Well-articulated.
  • Minimal, focused fix: One function, 4 lines changed, no unnecessary refactoring.
  • Dead code removal: Removing the len(stopTimes) > 0 guard inside the loop (always true) is a nice cleanup.
  • Correct caller interaction: Returning "" is properly handled at line 113 (if nextStopID != "") — the NextStop field simply isn't set, which is the right behavior for a completed trip.

@aditya-systems-hub
Copy link
Author

Hi @aaronbrethorst ,

Thank you so much for the detailed review — I really appreciate it.
You’re absolutely right about adding a regression test. That makes complete sense, especially since this was a silent bug and the function didn’t have any coverage before. I’ll add the three test cases you suggested to make sure this behavior doesn’t accidentally regress in the future.
I’ll also go ahead and remove the unused api parameter and update the call site to keep things clean, since I’m already touching the function.
Once that’s done, I’ll rerun tests, lint, and formatting checks to make sure everything passes cleanly and keep it as a single commit. Thanks again for the thoughtful feedback — it really helps improve the quality of the change.

@aditya-systems-hub
Copy link
Author

aditya-systems-hub commented Feb 23, 2026

@aaronbrethorst ,
Thanks for the detailed feedback — really appreciate it.
I’ve added the regression test for findNextStop() covering all three cases you suggested (normal case, last stop, and nil vehicle). I also removed the unused api parameter and updated the call site in BuildTripStatus() to keep things clean while already touching the function.
I ran build, vet, lint, and the new test locally — everything related to this change passes cleanly.
Please let me know if you’d like any further adjustments before merging.

Copy link
Member

@aaronbrethorst aaronbrethorst left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey Aditya, you've nailed the two items from my first review — the three-case regression test for findNextStop is exactly what was needed, and removing the unused api parameter is a nice cleanup. The core bugfix itself remains correct and well-reasoned.

However, I've found two critical issues that need to be resolved before merging. They're both mechanical fixes — the logic of your change is solid.

Critical

1. CRLF line endings will rewrite both files entirely

Both trips_helper.go and trips_helper_test.go have CRLF (\r\n) line endings, while the rest of the codebase uses LF (\n). This causes GitHub to show 875 additions / 875 deletions for the helper file — the entire file appears rewritten even though only ~10 lines actually changed. More importantly, introducing CRLF into LF files creates inconsistency and guarantees merge conflicts with every other branch touching these files.

Fix: Convert both files back to LF endings. On macOS/Linux:

sed -i 's/\r$//' internal/restapi/trips_helper.go
sed -i 's/\r$//' internal/restapi/trips_helper_test.go

Or configure your editor to use LF for this project. You may also want to add a .gitattributes file or configure your local git: git config core.autocrlf input.

2. Rebase needed — merging will silently remove another contributor's error logging

Because the files are CRLF-rewritten, git treats them as entirely new content. Your branch was created before commit fe3642e ("fix: refine alerts logging and sync doc comments") landed on main, which added important error logging to GetSituationIDsForTrip:

// This code exists on main but NOT on your branch:
} else if !errors.Is(err, sql.ErrNoRows) {
    api.Logger.Warn("Failed to fetch route for alerts; degrading to trip+route matching only",
        slog.String("trip_id", tripID),
        slog.String("route_id", routeID),
        slog.Any("error", err),
    )
}

Merging your PR as-is would quietly overwrite this logging and its associated imports (database/sql, errors, log/slog). That's a regression of another contributor's work.

Fix: After converting to LF endings, rebase on latest main:

# First fix line endings (step 1 above), then:
git fetch origin
git rebase origin/main

The rebase should apply cleanly once the line endings match, since your changes and the logging changes touch different parts of the file. If you run into trouble, here's a guide that may help: https://www.brethorsting.com/blog/2026/01/git-rebase-for-the-terrified/

Verification

After fixing both issues, please confirm:

  • make test passes
  • make lint reports 0 issues
  • go fmt ./... produces no changes
  • git diff origin/main --stat shows a small, focused diff (not 875+/875-)

The actual code changes — the bounds-check fix in findNextStop, the regression test, and the api parameter removal — are all correct. Once the line endings and rebase are sorted, this should be good to go.

Remove the unused api *RestAPI parameter from findNextStop() as flagged
by the reviewer. Update the single call site in BuildTripStatus() to
match the new signature.

Add TestFindNextStop with three cases to prevent regression of the
modulo wrap-around bug: correct next stop mid-trip, empty string at
last stop, and empty string when vehicle is nil.
@aditya-systems-hub aditya-systems-hub force-pushed the fix/find-next-stop-wraparound branch from 45e6042 to 1bb669c Compare February 24, 2026 01:29
@aditya-systems-hub
Copy link
Author

Hello Aaron,
I’ve fixed the line endings and rebased on the latest main. The logging changes are preserved and the diff is now clean and focused.
Thanks again for the detailed review — I really enjoyed working through this and learned a lot in the process!

Copy link
Member

@aaronbrethorst aaronbrethorst left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey Aditya, nice work addressing the CRLF and rebase issues from the last round — the diff is now clean and focused, and the alerts logging from the other contributor is properly preserved. Just one more thing to sort out before we can merge:

Critical

1. Tests won't compile — findNextStop signature mismatch (trips_helper_test.go:28)

The test calls findNextStop(stopTimes, vehicle) with 2 arguments, but the function signature still has 3 parameters:

// Current signature (trips_helper.go:294):
func findNextStop(
    api *RestAPI,
    stopTimes []*gtfsdb.StopTime,
    vehicle *gtfs.Vehicle,
) (stopID string, offset int) {
// Test calls (trips_helper_test.go:28, 34, 39):
stopID, _ := findNextStop(stopTimes, vehicle)  // Missing 'api' argument

This means go test ./internal/restapi/... will fail with a compilation error. It looks like the api parameter removal you mentioned in your earlier comment got lost during the CRLF fix and rebase. To fix this, you have two options:

Option A — Remove the unused api parameter (recommended, since it's unused):

// trips_helper.go: Remove 'api *RestAPI' from the signature
func findNextStop(
    stopTimes []*gtfsdb.StopTime,
    vehicle *gtfs.Vehicle,
) (stopID string, offset int) {

// trips_helper.go line 105: Update the call site
nextStopID, nextOffset = findNextStop(stopTimesPtrs, vehicle)

Option B — Add api back to the test calls:

// But this option leaves dead code, so Option A is preferred
stopID, _ := findNextStop(api, stopTimes, vehicle)

After fixing, please confirm go build ./... and make test both pass cleanly.

Thanks again, and I look forward to merging this change.

Fix type mismatch by passing stopTimesPtrs instead of stopTimes to findNextStop.
Add MSYSTEM detection in Makefile so CGO env vars use Unix syntax in MSYS2
instead of Windows CMD syntax, making make test work in MSYS2 MINGW64.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants